<?php
/**
 * Class model that provides the base functionality to manage Entity data
 *
 * This class was defined following the indications of
 * {@link http://www.overzealous.com/dmz/index.html DMZ ORM}. So,
 * this manage inserts, updates and deletes sql operations, as well as fully
 * set of instructions that can be used to make queries. And much more
 *
 * @author Gonzalez Federico, Viera Leonel
 * @package Protoweb
 * @subpackage model
 */
class Entity extends PwModel 
{
    public $table      = "entities";
    public $model      = "entity";
    public $has_many   = array("attribute");
    public $has_one    = array("project");
    public $validation = array(
        "singular" => array(
            "label" => "Singular",
            "rules" => array("trim", "strtolower", "required", "unique_pair" => "project_id")
        ),
        "plural" => array(
            "label" => "Plural",
            "rules" => array("trim", "strtolower", "required", "unique_pair" => "project_id")
        ),
        "description" => array(
            "label" => "Description",
            "rules" => array("trim")
        ),
        'one' => array(
            'label' => 'Has One',
            'rules' => array('trim')
        ),
        'many' => array(
            'label' => 'Has Many',
            'rules' => array('trim')
        )

    ); // $validation

    /**
     * Constructor of this class
     * 
     * @param int $id Identifier of an entity.
     * @return Entity
     */
    public function __construct($id = null)
    {
        parent::__construct($id);
    }

    public function getViews()
    {
         // get CodeIngiter instace
        $CI =& get_instance();

        // get core actions template path
        $allViewsTemplates = scandir($this->project->viewsTemplatesPath());

        // remove '.' and '..' entries and reconstruct array index
        $allViewsTemplates = array_diff($allViewsTemplates, array('.','..'));
        $allViewsTemplates = array_values($allViewsTemplates);

        // build array with views templates from this entity
        $tplArray = array();
        foreach ($allViewsTemplates as $fileName) {
            if (strpos($fileName, "{$this->singular}_") === 0) {
                array_push($tplArray, basename($fileName,'.tpl'));
            }
        }

        return $tplArray;
    }

    public function getReflectedController()
    {
        exec("php -l {$this->customControllerFilePath()}",$output,$exitCode);

        if ($exitCode === 0) {
            require_once $this->customControllerFilePath();
            return new ReflectionClass($this->plural);
        }
        else {
            return array(
                'output'    => implode("<br />",$output), 
                'exit_code' => $exitCode
            );
        }
    }

    public function getTableName()
    {
        return $this->project->slug . '.' . $this->plural;
    }

    public function getHasOne()
    {
        $oneToArray = $this->getOne();

        if (count($oneToArray) > 0) {

            $hasOneRelated = new Entity();
            $hasOneRelated->where_related_project('id',$this->project->id)
                          ->group_start();

            foreach ($oneToArray as $oneID) {
                $hasOneRelated->or_where('id',$oneID);
            }

            $hasOneRelated->group_end();

            return $hasOneRelated->get();
        }
        else {
            return array();
        }
    }

    public function getHasMany()
    {
        $hasManyIDs = $this->getMany();

        if (count($hasManyIDs)) {

            $hasManyRelated = new Entity();

            $hasManyRelated->where_related_project('id',$this->project->id)
                           ->group_start();

            foreach ($hasManyIDs as $manyID) {
                $hasManyRelated->or_where('id',$manyID);
            }

            $hasManyRelated->group_end();

            return $hasManyRelated->get();
        }
        else {
            return array();
        }
    }

    public function isRelatedTo($entityID, $type = 'hasone')
    {
        $relatedEntities;

        if ($type === 'hasone') {
            $relatedEntities = $this->getOne();
        }
        else {
            $relatedEntities = $this->getMAny();
        }

        if (is_array($relatedEntities)) {
            return in_array($entityID, $relatedEntities);
        }
        else {
            return false;
        }
    }

    public function modelFilePath()
    {
        return $this->project->models_path() . 'protoweb/' . $this->singular . '_protoweb.php';
    }

    public function customModelFilePath()
    {
        return $this->project->models_path() . $this->singular . '.php';
    }

    public function controllerFilePath()
    {
        return $this->project->controllers_path() . 'protoweb/' . $this->plural . '_protoweb.php';
    }

    public function customControllerFilePath()
    {
        return $this->project->controllers_path() . $this->plural . '.php';
    }

    public function generateModelFiles($generateCustomModel) 
    {
        $CI                      =& get_instance();
        $data['entity']          = $this;
        $data['has_validations'] = false;

        //checking if at least one attribute has rules
        foreach ($this->attribute as $attr) {
            if (strlen($attr->rules) > 0) {
                $data['has_validations'] = true;
            }
        }

        $CI->smartyview->smarty->assign($data);

        // Protoweb Model
        $model = $CI->smartyview->smarty->fetch($CI->config->item('model_template'));
        file_put_contents($this->modelFilePath(), $model);
        chmod($this->modelFilePath(), 0777);

        // Custom Model
        if ($generateCustomModel) {
            $customModel = $CI->smartyview->smarty->fetch($CI->config->item('custom_model_template'));
            file_put_contents($this->customModelFilePath(), $customModel);
            chmod($this->customModelFilePath(), 0777);
        }
    }

    public function generateControllerFiles($generateCustomController)
    {
        $CI             =& get_instance();
        $data['entity'] = $this;
        $CI->smartyview->smarty->assign($data);

        $controller = $CI->smartyview->smarty->fetch($CI->config->item('controller_template'));
        file_put_contents($this->controllerFilePath(), $controller);
        chmod($this->controllerFilePath(), 0777);

        // custom controller
        if ($generateCustomController) {
            $customController = $CI->smartyview->smarty->fetch($CI->config->item('custom_controller_template'));
            file_put_contents($this->customControllerFilePath(), $customController);
            chmod($this->customControllerFilePath(), 0777);
        }
    }

    public function firstSave($project)
    {
        $this->trans_begin();

        $CI =& get_instance();
        $CI->load->dbforge();

        try {

            if ( ! $this->save($project)) {
                throw new Exception($this->error->string);
            }

            // buffering autoincremental id for the table creation
            $fields = array(
                'id' => array(
                    'type'           => 'INT',
                    'constraint'     => 10,
                    'unsigned'       => true,
                    'auto_increment' => true
                )
            );

            $CI->dbforge->add_field($fields);
            $CI->dbforge->add_key('id', true);

            // Create table with an autoincremental id
            $CI->dbforge->drop_table($this->getTableName());
            if ( ! $CI->dbforge->create_table($this->getTableName())) {
                throw new Exception("Can't create table in " . $project->name . "'s DB");
            }

            // Generate model files for the created entity
            $this->generateModelFiles(true);

            // Generate controllers files for the created entity
            $this->generateControllerFiles(true);

            return true;
        }
        catch (Exception $exc) {
            //Rollback Protoweb's DB changes and delete projects' table
            $this->trans_rollback();

            if ($this->plural > '') {
                $CI->dbforge->drop_table($this->getTableName());
            }

            $this->error->string = $exc->getMessage();

            return false;
        }
    } // firstSave

    public function getOne()
    {
        if (json_decode($this->one)) {
           return (json_decode($this->one));
        }
        else {
            return array();
        }
    }

    public function getMany()
    {
        if (json_decode($this->many)) {
           return json_decode($this->many);
        }
        else {
            return array();
        }
    }

    public function addHasOneRelated($entityID)
    {
        $oneArray = $this->getOne();
        array_push($oneArray,$entityID);
        $this->one = json_encode($oneArray);
        $this->save();

        $entity = new Entity($entityID);

        $column[$entity->singular . '_id'] = array(
            'type'       => 'INT',
            'constraint' => 10,
            'unsigned'   => true
        );

        $CI =& get_instance();
        $CI->load->dbforge();
        $CI->dbforge->add_column($this->getTableName(), $column);
    }

    public function removeHasOneRelated($entityID)
    {
        $oneArray  = array_diff($this->getOne(), array($entityID));
        $this->one = json_encode(array_values($oneArray));
        $this->save();

        $entity = new Entity($entityID);

        $CI =& get_instance();
        $CI->load->dbforge();
        $CI->dbforge->drop_column($this->getTableName(), $entity->singular . '_id');
    }

    public function addHasManyRelated($entityID)
    {
        $manyArray = $this->getMany();
        array_push($manyArray,$entityID);
        $this->many = json_encode($manyArray);
        $this->save();

        $entity = new Entity($entityID);

        // if ralationship is many to many, generate the join table
        if ($entity->isRelatedTo($this->id,'hasmany')) {

            $field_name1 = $this->singular . '_id';
            $field_name2 = $entity->singular . '_id';

            $fields = array(
                'id' => array(
                    'type'           => 'INT',
                    'constraint'     => 10,
                    'unsigned'       => TRUE,
                    'auto_increment' => TRUE
                ),
                $field_name1 => array(
                    'type'       => 'INT',
                    'constraint' => 10,
                    'unsigned'   => TRUE,
                    'null'       => FALSE
                ),
                $field_name2 => array(
                    'type'       => 'INT',
                    'constraint' => 10,
                    'unsigned'   => TRUE,
                    'null'       => FALSE
                )
            );

            $CI =& get_instance();
            $CI->load->dbforge();
            $CI->dbforge->add_field($fields);
            $CI->dbforge->add_key("id", TRUE);
            $CI->dbforge->create_table(
                $this->project->slug . '.' . $this->generateJoinTableName($this->plural, $entity->plural)
            );
        }
    }

    public function removeHasManyRelated($entityID)
    {
        $manyArray  = array_diff($this->getMany(), array($entityID));
        $this->many = json_encode(array_values($manyArray));
        $this->save();

        $entity = new Entity($entityID);

        // if ralationship was many to many, we generate the join table
        if ($entity->isRelatedTo($this->id,'hasmany')) {
            $CI =& get_instance();
            $CI->load->dbforge();
            $CI->dbforge->drop_table(
                $this->project->slug . '.' . $this->generateJoinTableName($this->plural, $entity->plural)
            );
        }
    }

    private function updateHasManyRelationships($hasManyIDs)
    {
        if (count($hasManyIDs) > 0) {
            // Creating new relationships
            foreach ($hasManyIDs as $relatedID) {
                if ( ! $this->isRelatedTo($relatedID, 'hasmany')) {
                    $this->addHasManyRelated($relatedID);
                }
            }

            // Deleting has_one relationships
            foreach ($this->getHasMany() as $manyRelateted) {
                if ( ! in_array($manyRelateted->id, $hasManyIDs)) {
                    $this->removeHasManyRelated($manyRelateted->id);               
                }
            }
        }
        else {
            // Deleting all existing has_one relationships
            foreach ($this->getHasMany() as $manyRelateted) {
                $this->removeHasManyRelated($manyRelateted->id);               
            }
        }
    }

    private function updateHasOneRelationships($hasOneIDs)
    {
        if (count($hasOneIDs) > 0) {
            // Creating new relationships
            foreach ($hasOneIDs as $relatedID) {
                if ( ! $this->isRelatedTo($relatedID, 'hasone')) {
                    $this->addHasOneRelated($relatedID);
                }
            }

            // Deleting has_one relationships
            foreach ($this->getHasOne() as $oneRelateted) {
                if ( ! in_array($oneRelateted->id, $hasOneIDs)) {
                    $this->removeHasOneRelated($oneRelateted->id);               
                }
            }
        }
        else {
            // Deleting all existing has_one relationships
            foreach ($this->getHasOne() as $oneRelateted) {
                $this->removeHasOneRelated($oneRelateted->id);               
            }
        }
    }

    public function updateRelationships($hasOneIDs, $hasManyIDs)
    {
        if (is_array($hasOneIDs)) {
            $this->updateHasOneRelationships($hasOneIDs);
        }
        else {
            $this->updateHasOneRelationships(array());   
        }

        if (is_array($hasManyIDs)) {
            $this->updateHasManyRelationships($hasManyIDs);
        }
        else {
            $this->updateHasManyRelationships(array());   
        }

        // Regenerate model file
        $this->generateModelFiles(false);
    }

    /**
     * Given two table names, generates a joining table name respecting the rules
     * imposed by Datamapper
     *
     * @param string $table1
     * @param string $table2
     * @return string the generated joining table name
     */
    private function generateJoinTableName($table1, $table2)
    {
        if (strcmp($table1, $table2) < 0) {
            return($table1 . '_' . $table2);
        }
        else {
            return($table2 . '_' . $table1);
        }
    }

    /**
     * Remanes generated project's join tables
     *
     * This method rename join table's name, when plural is changed.
     *
     * @param string $old_plural
     * @param string $new_plural
     * @return boolean
     */
    private function renameJoinTables($old_plural, $new_plural) {
        $joinTablesToRename = array();

        // Constructs an array of old and new join table names
        foreach ($this->getHasMany() as $hasManyEntity) {
            if ($hasManyEntity->isRelatedTo($this->id,'hasmany')) {
                
                // Get the old and new join table names
                $entry['old'] = $this->generateJoinTableName($old_plural, $hasManyEntity->plural);
                $entry['new'] = $this->generateJoinTableName($new_plural, $hasManyEntity->plural);

                array_push($joinTablesToRename, $entry);    
            }
        }

        // finally rename table
        $CI =& get_instance();
        $CI->load->dbforge();

        foreach ($joinTablesToRename as $table) {
            if ( ! $CI->dbforge->rename_table(
                        $this->project->slug . '.' . $table['old'],
                        $this->project->slug . '.' . $table['new']
                    ) ) {
                return false;
            }
        }

        return true;
    } // renameJoinTables

    public function saveAndRefactor($newPlural, $newSingular, $newDescription)
    {
        // Clone current instance
        $old = $this->get_clone();

        $CI =& get_instance();
        $CI->load->dbforge();

        $this->trans_begin();
        
        try {

            $this->plural      = $newPlural;
            $this->singular    = $newSingular;
            $this->description = $newDescription;

            if ( ! $this->save()) {
                throw new Exception($this->error->string);
            }

            // if the table name of the entity was changed:
            // - Update entity's table name in generated project
            // - Update generated project's join tables that involve the modified entity
            // - Rename controller files
            // - Replace ocurrences of old plural in controller files
            // 
            if ($this->plural != $old->plural) {

                // Rename table name
                if ( ! $CI->dbforge->rename_table(
                            $this->project->slug . '.' . $old->plural,
                            $this->project->slug . '.' . $this->plural
                        ) ) {
                    throw new Exception("Can't change table name from {$old->plural} to {$this->plural}");
                }

                // Rename join table names
                $this->renameJoinTables($old->plural, $this->plural);

                // Rename Controllers
                if (file_exists($old->customControllerFilePath())) {
                    rename($old->customControllerFilePath(),$this->customControllerFilePath());
                }

                if (file_exists($old->controllerFilePath())) {
                    rename($old->controllerFilePath(),$this->controllerFilePath());
                }

                // Replace ocurrences in controllers
                $controller = file_get_contents($this->controllerFilePath());
                $controller = str_replace($old->plural, $this->plural, $controller);
                $controller = str_replace(ucfirst($old->plural), ucfirst($this->plural), $controller);
                file_put_contents($this->controllerFilePath(), $controller);

                $customController = file_get_contents($this->customControllerFilePath());
                $customController = str_replace($old->plural, $this->plural, $customController);
                $customController = str_replace(ucfirst($old->plural), ucfirst($this->plural), $customController);
                file_put_contents($this->customControllerFilePath(), $customController);
            } // if ($this->plural != $old->plural)
            
            // Commit changes
            $this->trans_commit();

            // if the model name of the entity was changed:
            // - Modify columns from generated project DB, that refer to this entity
            // - Rename custom model file
            // - Replace old singular for new singular in custon model file.
            // - Delete protoweb model file
            // - Regenerate all protoweb model files
            // 
            if ($old->singular != $this->singular) {
                
                // Rename columns from generated project DB, that refer to this entity
                foreach ($this->project->entity as $entity) {
                    if ($entity->isRelatedTo($this->id, 'hasone')) {
                        
                        $field["{$old->singular}_id"] = array(
                            'name' => $this->singular . '_id',
                            'type' => 'INT( 10 ) UNSIGNED'
                        );

                        $CI->dbforge->modify_column(
                            $this->project->slug . '.' . $entity->plural,
                            $field
                        );
                    }
                }
                
                // Rename custom model file
                if (file_exists($old->customModelFilePath())) {
                    rename($old->customModelFilePath(),$this->customModelFilePath());                
                }

                // Replace ocurrences in custom model file
                $customModel = file_get_contents($this->customModelFilePath());
                $customModel = str_replace($old->singular, $this->singular, $customModel);
                $customModel = str_replace(ucfirst($old->singular), ucfirst($this->singular), $customModel);
                file_put_contents($this->customModelFilePath(), $customModel);

                // Delete protoweb model file (with the old singular)
                if (file_exists($old->modelFilePath())) {
                    unlink($old->modelFilePath());
                }

                // Regenerate all protoweb models
                $this->project->generateEntityModels(false);

            } // if ($old->singular != $this->singular)

            return true;
        }
        catch (Exception $exc) {

            //Rollback Protoweb's DB changes and delete projects' table
            $this->trans_rollback();
            $this->error->string = $exc->getMessage();

            return false;
        }
    } // saveAndRefactor


    /**
     * Returns an array with the names of the models that are not ralated 
     * to the instance
     *
     * @return array
     */
    public function getNoRelated()
    {
        $noRelated = array();
        $oneArray  = $this->getOne();
        $manyArray = $this->getMany();

        foreach($this->project->entity as $ent) {
            if ( ($ent->id != $this->id) and
                 ( ! in_array($ent->id, $oneArray)) and
                 ( ! in_array($ent->id, $manyArray))
               ) {
                array_push($noRelated, $ent);
            }
        }
        return($noRelated);
    }

    public function deleteView($viewName)
    {

        $templateView = $this->project->viewsTemplatesPath() . $viewName . '.tpl';
        $compiledView = $this->project->views_path() . $viewName . '.html';

        if (file_exists($templateView)) {
            unlink($templateView);
        }

        if (file_exists($compiledView)) {
            unlink($compiledView);
        }
    }

    public function deleteAllViews()
    {
        foreach ($this->getViews() as $view) {
            $this->deleteView($view);
        }
    }

    public function remove()
    {

        $CI =& get_instance();
        $CI->load->dbforge();
        
        // drop entity's table
        $CI->dbforge->drop_table($this->getTableName());

        // remove one relationship to this entity
        foreach ($this->project->entity as $entity) {
            if ($entity->isRelatedTo($this->id,'hasone')) {
                $entity->removeHasOneRelated($this->id);
            }
        }

        // remove many relationship to this entity
        foreach ($this->project->entity as $entity) {
            if ($entity->isRelatedTo($this->id,'hasmany')) {
                $entity->removeHasManyRelated($this->id);
            }
        }

        // delete pw model file
        if (file_exists($this->modelFilePath())) {
            unlink($this->modelFilePath());
        }

        // delete custom model file
        if (file_exists($this->customModelFilePath())) {
            unlink($this->customModelFilePath());
        }

        // delete controller file
        if (file_exists($this->controllerFilePath())) {
            unlink($this->controllerFilePath());
        }

        // delete custom controller file
        if (file_exists($this->customControllerFilePath())) {
            unlink($this->customControllerFilePath());
        }
        
        // delete views. Tpls and htmls
        $this->deleteAllViews();

        // delete from pw db
        $this->delete();
    }

    public function compileView($viewName)
    {
        // Library to manipulate DOM objects similar to jquery
        $this->load->library('Simple_html_dom.php');

        //file_get_html($url, $use_include_path = false, $context=null, $offset = -1, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT)
        $html = file_get_html($this->project->viewsTemplatesPath() . $viewName . '.tpl', NULL, NULL, NULL, NULL, FALSE, NULL, NULL, FALSE, NULL);

        // Count components
        $componentsCount = 0;
        foreach ($html->find('pw:component') as $component) {
            $componentsCount++;
        }

        // parse each component
        for ($i=0; $i < $componentsCount; $i++) { 

            // always get the first component, because we are deleting one every iteration
            $element = $html->find("pw:component",0);

            // get component object
            $component = Component::getComponent($element->attr['name'], $this->project);
            $data      = null;

            // prepare data array with component's attrbibutes
            foreach ($component->attributes as $attribute) {
                if (array_key_exists("data-{$attribute['id']}", $element->attr)) {
                    $data[$attribute['id']] = $element->attr["data-{$attribute['id']}"];
                }
            }
            
            // get component container (if present)
            $data['pwContainerContent'] = $element->find('pw:container',0);
            $data['pwContainerContent'] = (isset($data['pwContainerContent']) ? $data['pwContainerContent']->innertext : '');

            // get component design view
            $element->outertext = $component->parse($data); 

            // Backup html into string
            $str_html_bkp = $html->root->innertext;

            // Free memory
            $html->clear();
            unset($html);

            // create new dom object from backup
            $html = str_get_html($str_html_bkp, false, false, DEFAULT_TARGET_CHARSET, false, DEFAULT_BR_TEXT);
        }
/*
        // pw:component to span
        foreach ($html->find('pw:component') as $element) {
            $component = Component::getComponent($element->attr['name'], $this->project);
            $data = null;
            foreach ($component->attributes as $attribute) {
                if (array_key_exists("data-{$attribute['id']}", $element->attr)) {
                    $data[$attribute['id']] = $element->attr["data-{$attribute['id']}"];
                }
            }
            $element->outertext = $component->parse($data);
        }
*/
        $parsedTpl = $html->__toString();

        // add DOCTYPE and replace html, head and body
        $parsedTpl = "<!DOCTYPE html>\n" . $parsedTpl;
        $parsedTpl = str_replace('pw:html', 'html', $parsedTpl);
        $parsedTpl = str_replace('pw:head', 'head', $parsedTpl);
        $parsedTpl = str_replace('pw:body', 'body', $parsedTpl);

        file_put_contents($this->project->views_path() . $viewName . '.html', $parsedTpl);
    }

    public function compileAllViews()
    {
        foreach ($this->getViews() as $view) {
            $this->compileView($view);
        }
    }

    public function getHasManyToMany()
    {
        $manyToMany = array();
        foreach ($this->getHasMany() as $manyRelateted) {
            if ($manyRelateted->isRelatedTo($this->id,'hasmany')) {
                array_push($manyToMany, $manyRelateted);
            }
        }
        return $manyToMany;
    }
} // Class Entity

/* End of file entity.php */
/* Location: ./application/models/entity.php */
